<?php

/**
 * Retriever job to fetch mails from a mailbox
 *
 * @author tobias.vogel
 */
class Mailfetcher_ImapRetriever {

	/**
	 * @var Mailfetcher_DbFsStorage Storer to store messages
	 */
	private $storage;

	/**
	 * @var resource IMAP connection 
	 */
	private $connection;
	
	/**
	 * @var array Collected messages from fetching
	 */
	public $messages = array();

	/**
	 * Constructs a new message retriever
	 */
	public function __construct() {
		$this->storage = new Mailfetcher_DbFsStorage();
		ezcMailCharsetConverter::setConvertMethod(array('Mailfetcher_Utf8Converter', 'mbStringConvertToUtf8'));
	}

	/**
	 * Retrieves mails for all configured mailboxes. 
	 */
	public function retrieveAllMailboxes() {

		$mailboxes = Db_Mailboxes::findAll();
		
		if (!$mailboxes) {
			$this->messages[] = "No mailboxes configured yet.";
			
		} else {
			foreach ($mailboxes as $mailbox) {
				$this->messages[] = "Fetching mails for user {$mailbox->mbx_user}";
				$this->retrieve($mailbox);
			}
		}
	}

	/**
	 * Retrievs mail of a certain mailbox
	 *
	 * @param Db_Mailboxes $mailbox The mailbox config to retrieve from
	 */
	public function retrieve($mailbox) {
		$this->connection = imap_open($mailbox->getDsn(), $mailbox->mbx_user, $mailbox->mbx_pass);

		$this->recalculateFetchDate($mailbox);
		$uidsToFetch = $this->lookupUidsToFetch($mailbox);

		$parser = new ezcMailParser();

		foreach ($uidsToFetch as $uid) {
			$this->messages[] = "Fetching message with UID {$uid}";

			// fetch raw mail source into a variable
			$message = imap_fetchheader($this->connection, $uid, FT_UID);
			$message .= imap_body($this->connection, $uid, FT_UID);

			// parse a single message (needs to be in a set)
			$messageSet = new ezcMailVariableSet($message);
			$messages = $parser->parseMail($messageSet);

			$this->storage->storeMessage($mailbox, $messages[0]);
		}
		
		// Add storage messages
		$this->messages = array_merge($this->messages, $this->storage->messages);

		// Add result message
		$noOfMessages = count($uidsToFetch);
		$this->messages[] = "Fetched {$noOfMessages} messages";

		// save updated mailbox (e.g. for changed last_fetch_date)
		$mailbox->save();

		imap_close($this->connection);
	}

	/**
	 * Advances the mailbox's last fetch date on a daily basis, depending upon if it's
	 * still further in the past. This allows for incremental fetching of mailboxes even
	 * when far behind.
	 * 
	 * @param Db_Mailboxes $mailbox The mailbox to modify
	 */
	private function recalculateFetchDate($mailbox) {
		$timeYesterday = strtotime('-1 day', time());

		if ($mailbox->mbx_last_fetch > 0) {
			// if we're still in the past, advance one day
			if ($mailbox->mbx_last_fetch < $timeYesterday) {
				$mailbox->mbx_last_fetch = strtotime('+1 day', $mailbox->mbx_last_fetch);
			}
		} else {
			// if no useful date has been supplied in the config, start with yesterday
			$mailbox->mbx_last_fetch = $timeYesterday;
		}
	}

	/**
	 * Builds an IMAP search query to fetch one whole day of messages, depending
	 * upon the last fetch time of the mailbox.
	 * 
	 * @param Db_Mailboxes $mailbox The mailbox to build the query for
	 * @return array IMAP search query for getting  
	 */
	private function buildSearchQuery($mailbox) {
		$timeLowerBoundary = $mailbox->mbx_last_fetch;
		$timeUpperBoundary = strtotime('+1 day', $timeLowerBoundary);

		$dateLowerBoundary = date("d-M-Y", $timeLowerBoundary);
		$dateUpperBoundary = date("d-M-Y", $timeUpperBoundary);

		return 'SINCE ' . $dateLowerBoundary . ' BEFORE ' . $dateUpperBoundary;
	}

	/**
	 * Scans a mailbox for all messages, that have not yet been retrieved.
	 * 
	 * @param Db_Mailboxes $mailbox The mailbox to look up for new messages
	 * @return array List of all message UIDs, that have not been fetched yet
	 */
	private function lookupUidsToFetch($mailbox) {
		$messageQuery = $this->buildSearchQuery($mailbox);
		$this->messages[] = "Using message query: {$messageQuery}";

		$messages = imap_search($this->connection, $messageQuery, SE_UID);

		$uidsToFetch = array();

		// check, if we even have results to fetch headers for
		if ($messages !== false) {

			// fetch message headers of search result, to check for new message ids
			$msgOverviews = imap_fetch_overview($this->connection, implode(',', $messages), FT_UID);

			// TODO: maybe batch fetch with WHERE ml_message_id IN (...)
			// Should be implemented with care, as it's possible to exceed the
			// maximum query length depending upon the message count
			foreach ($msgOverviews as $msgOverview) {

				$search = array(
					'mbx_mailbox_id' => $mailbox->id,
					'ml_message_id' => $msgOverview->message_id,
				);

				$mail = Db_Mails::findByFields($search);

				if (!$mail) {
					$uidsToFetch[] = $msgOverview->uid;
				}
			}
		}

		return $uidsToFetch;
	}

}
